1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.font.ttf; 12 import hip.api.data.font; 13 14 immutable dstring defaultCharset = " \náéíóúãñçabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\\|'\"`/*-+,.;:´_=!@#$%&()[]{}~^?"; 15 16 version = HipArsdFont; 17 18 19 private uint nextPowerOfTwo(uint number) 20 { 21 ulong value = 1; 22 while(value < number) 23 { 24 value <<= 1; 25 } 26 return cast(uint)value; 27 } 28 29 private int round(float f) 30 { 31 return cast(int)(f+0.5); 32 } 33 34 class HipNullFont : HipFont 35 { 36 string path; 37 this(){} 38 this(string path, uint fontSize = 32){} 39 override uint getHeight() const { return 0; } 40 41 /** 42 * This will cause a full load of the .ttf file, image generation and GPU upload. Should only be used 43 * If you don't care about async 44 */ 45 bool loadFromMemory(in ubyte[] data){return false;} 46 bool partialLoad(in ubyte[] data, out ubyte[] rawImage){return false;} 47 bool loadTexture(ubyte[] rawImage){return false;} 48 override int getKerning(dchar current, dchar next) const{return 0;} 49 override int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const{return 0;} 50 override HipFont getFontWithSize(uint size){return new HipNullFont();} 51 } 52 53 54 version(HipArsdFont) 55 /** 56 * Check the unicode table: https://unicode-table.com/en/blocks/ 57 * There is a lot of character ranges that defines a set of characters in a language, such as: 58 * 0000—007F Basic Latin 59 * 0080—00FF Latin-1 Supplement 60 * 0100—017F Latin Extended-A 61 * 0180—024F Latin Extended-B 62 * Maybe it will prove more useful than having a default charset 63 */ 64 class HipArsd_TTF_Font : HipFont 65 { 66 import arsd.ttf; 67 protected float fontScale; 68 protected TtfFont font; 69 string path; 70 protected uint fontSize = 32; 71 protected uint _textureWidth, _textureHeight; 72 protected Hip_TTF_Font mainInstance; 73 protected Hip_TTF_Font[] clones; 74 75 this(string path, uint fontSize = 32) 76 { 77 this.path = path; 78 this.fontSize = fontSize; 79 } 80 override uint getHeight() const { return fontSize; } 81 /** 82 * This will cause a full load of the .ttf file, image generation and GPU upload. Should only be used 83 * If you don't care about async 84 */ 85 bool loadFromMemory(in ubyte[] data) 86 { 87 if(data == null || data.length == 0) 88 return false; 89 try{font = TtfFont(data);} 90 catch(Exception e){return false;} 91 return loadTexture( 92 generateImage(fontSize, _textureWidth, _textureHeight) 93 ); 94 } 95 96 bool partialLoad(in ubyte[] data, out ubyte[] rawImage) 97 { 98 font = TtfFont(data); 99 rawImage = generateImage(fontSize, _textureWidth, _textureHeight); 100 return true; 101 } 102 103 override int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const 104 { 105 return cast(int)(fontScale*stbtt_GetGlyphKernAdvance(cast(stbtt_fontinfo*)&font.font, current.glyphIndex, next.glyphIndex)); 106 } 107 override int getKerning(dchar current, dchar next) const 108 { 109 return cast(int)(fontScale*stbtt_GetCodepointKernAdvance(cast(stbtt_fontinfo*)&font.font, int(current), int(next))); 110 } 111 112 113 bool loadTexture(ubyte[] rawImage) 114 { 115 assert(rawImage !is null, "Must first generate a texture before uploading to GPU"); 116 import hip.assets.image; 117 import hip.assets.texture; 118 import hip.error.handler; 119 Image img = new Image(); 120 img.loadRaw(rawImage, _textureWidth, _textureHeight, 1); 121 HipTexture t = new HipTexture(null); 122 123 bool ret = t.load(img); 124 ErrorHandler.assertErrorMessage(ret, "Loading TTF", "Could not create texture for TTF"); 125 texture = t; 126 import core.memory; 127 GC.free(rawImage.ptr); 128 return ret; 129 } 130 131 /** 132 * This function returns a new font using the same data file, with a new size. 133 * The font data will reference to this same one 134 */ 135 override HipFont getFontWithSize(uint size) 136 { 137 Hip_TTF_Font ret = new Hip_TTF_Font(this.path, size); 138 ret.font = cast(TtfFont)this.font; 139 ret.mainInstance = cast(Hip_TTF_Font)(mainInstance is null ? this : mainInstance); 140 if(mainInstance) 141 mainInstance.clones~= ret; 142 else 143 clones~= ret; 144 145 if(!ret.loadTexture(ret.generateImage(size, ret._textureWidth, ret._textureHeight))) 146 return null; 147 148 return cast(HipFont)ret; 149 } 150 151 protected RenderizedChar renderCharacter(dchar ch, int size, float shift_x = 0.0, float shift_y = 0.0) 152 { 153 RenderizedChar rch; 154 rch.ch = ch; 155 rch.data = font.renderCharacter(ch, size, rch.width, rch.height, shift_x, shift_y); 156 return rch; 157 } 158 /** 159 * I'm no good packer. The image will be at least 2048xMinPowOf2 160 */ 161 protected ubyte[] generateImage(int size, out uint width, out uint height, dstring charset = defaultCharset) 162 { 163 if(charset.length == 0) 164 return null; 165 scope RenderizedChar[] fontChars = new RenderizedChar[charset.length]; //TODO: USe that as it is more optimised 166 scope(exit) 167 { 168 foreach(ch; fontChars) 169 ch.dispose(); 170 import core.memory; 171 GC.free(fontChars.ptr); 172 } 173 174 uint avgWidth = 0; 175 uint avgHeight = 0; 176 size_t i = 0; 177 foreach(dc; charset) 178 { 179 RenderizedChar rc = renderCharacter(dc, size); 180 avgWidth+= rc.width; 181 avgHeight+= rc.height; 182 fontChars[i++] = rc; 183 } 184 //Add as an error (pixel bleeding) 185 avgWidth = cast(uint)(avgWidth / charset.length) + 2; 186 avgHeight = cast(uint)(avgHeight / charset.length) + 2; 187 enum hSpacing = 1; 188 enum vSpacing = 1; 189 float x = 1; 190 float y = 0; 191 float optY = 0; 192 float scale = stbtt_ScaleForPixelHeight(&font.font, size); 193 194 //Setting details 195 fontScale = scale; 196 197 int ascent, descent, lineGap; 198 stbtt_GetFontVMetrics(&font.font, &ascent, &descent, &lineGap); 199 200 201 lineBreakHeight = cast(uint)(int(ascent - descent + lineGap) * scale); 202 203 //First guarantee the big size 204 import core.math:sqrt; 205 uint sqrtOfCharset = cast(uint)sqrt(cast(float)charset.length) + 1; 206 uint imageWidth = avgWidth * sqrtOfCharset; 207 uint imageHeight = avgHeight * sqrtOfCharset; 208 imageHeight = nextPowerOfTwo(imageHeight); 209 imageWidth = nextPowerOfTwo(imageWidth); 210 211 width = imageWidth; 212 height = imageHeight; 213 214 ubyte[] image = new ubyte[](imageWidth*imageHeight); 215 216 int largestHeightInRow = 0; 217 import hip.util.algorithm; 218 219 foreach(fontCh; quicksort(fontChars, (RenderizedChar a, RenderizedChar b) => a.height > b.height)) 220 { 221 int g = stbtt_FindGlyphIndex(&font.font, fontCh.ch); 222 int xAdvance, xOffset, yOffset, lsb; 223 int x1, y1; 224 stbtt_GetGlyphHMetrics(&font.font, g, &xAdvance, &lsb); 225 stbtt_GetGlyphBitmapBox(&font.font, g, scale,scale, &xOffset,&yOffset,&x1,&y1); 226 if(fontCh.ch == ' ') 227 { 228 int space_x0, space_x1; 229 if(fontCh.width == 0) 230 { 231 stbtt_GetCodepointBitmapBox(&font.font, int('n'), scale,scale, &space_x0, null, &space_x1, null); 232 spaceWidth = space_x1 - space_x0; 233 } 234 else 235 spaceWidth = fontCh.width; 236 } 237 238 if(x + fontCh.width + hSpacing > imageWidth) 239 { 240 x = hSpacing; 241 y+= largestHeightInRow + vSpacing; 242 largestHeightInRow = 0; 243 } 244 245 246 characters[fontCh.ch] = HipFontChar(fontCh.ch, cast(int)x, cast(int)y, fontCh.width, fontCh.height, 247 248 xOffset, yOffset, round(xAdvance*scale), 0, 0, 249 cast(float)x/imageWidth, cast(float)y/imageHeight, 250 cast(float)fontCh.width/imageWidth, cast(float)fontCh.height/imageHeight, 251 g 252 ); 253 fontCh.blitToImage(image, cast(int)(x), cast(int)(y), imageWidth, imageHeight); 254 x+= fontCh.width + hSpacing; 255 256 if(fontCh.height > largestHeightInRow) 257 largestHeightInRow = fontCh.height; 258 } 259 return image; 260 } 261 262 } 263 264 version(HipNullFont) 265 alias Hip_TTF_Font = HipNullFont; 266 else version(HipArsdFont) 267 alias Hip_TTF_Font = HipArsd_TTF_Font; 268 269 270 private struct RenderizedChar 271 { 272 dchar ch; 273 int size; 274 int width; 275 int height; 276 277 ubyte[] data; 278 279 void blitToImage(ref ubyte[] texture, int startX, int startY, int textureWidth, int textureHeight) 280 { 281 assert(startX + width < textureWidth, "Out of X boundaries"); 282 for(size_t i = 0; i < height; i++) 283 { 284 size_t pos = (startY+i)*textureWidth + startX; 285 assert(startY + i < textureHeight, "Out of Y boundaries"); 286 texture[pos..pos+width] = data[i*width..(i+1)*width]; 287 } 288 } 289 290 void dispose() 291 { 292 version(HipArsdFont) 293 { 294 if(data.ptr != null) 295 { 296 import arsd.ttf; 297 stbtt_FreeBitmap(data.ptr, null); 298 } 299 } 300 data = null; 301 } 302 }